<?php

namespace yunj\library\captcha;

use yunj\library\traits\Redis;

class Captcha {

    use Redis;

    // 验证码字符集合
    private $codeSet = '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY';

    // 验证码字体大小(px)
    protected $fontSize = 29;

    // 是否画混淆曲线
    protected $useCurve = true;

    // 是否添加杂点
    protected $useNoise = true;

    // 验证码位数
    private $length = 4;

    // 背景颜色（红、绿、蓝）
    protected $bg = [243, 251, 254];

    // 验证码图片实例
    private $im = null;
    // 验证码字体颜色
    private $color = null;
    // 验证码图片高度
    protected $imageH = 0;
    // 验证码图片宽度
    protected $imageW = 0;

    /**
     * 生成验证码图片并返回图像的base64内容和验证码hash值
     * @return array
     */
    public function generateImageBase64(): array {
        $code = $this->generateCode();

        // 图片宽(px)
        $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2;
        // 图片高(px)
        $this->imageH || $this->imageH = $this->fontSize * 2.5;
        // 建立一幅 $this->imageW x $this->imageH 的图像
        $this->im = imagecreate((int)$this->imageW, (int)$this->imageH);
        // 设置背景
        imagecolorallocate($this->im, $this->bg[0], $this->bg[1], $this->bg[2]);

        // 验证码随机字体
        $ttfPath = __DIR__ . '/assets/ttfs/';
        $dir = dir($ttfPath);
        $ttfs = [];
        while (false !== ($file = $dir->read())) {
            if (substr($file, -4) == '.ttf' || substr($file, -4) == '.otf') {
                $ttfs[] = $file;
            }
        }
        $dir->close();
        $fontttf = $ttfPath . $ttfs[array_rand($ttfs)];

        // 验证码字体随机颜色
        $this->color = imagecolorallocate($this->im, mt_rand(1, 150), mt_rand(1, 150), mt_rand(1, 150));

        if ($this->useNoise) {
            // 绘杂点
            $this->writeNoise();
        }
        if ($this->useCurve) {
            // 绘干扰线
            $this->writeCurve();
        }

        // 绘验证码
        $text = str_split($code['value']); // 验证码
        foreach ($text as $index => $char) {
            $x = $this->fontSize * ($index + 1) * 1.5;
            $y = $this->fontSize + mt_rand(10, 20);
            $angle = mt_rand(-40, 40); // 旋转角度
            imagettftext($this->im, $this->fontSize, $angle, (int)$x, (int)$y, $this->color, $fontttf, $char);
        }

        ob_start();
        // 输出图像
        imagepng($this->im);
        $content = ob_get_clean();
        imagedestroy($this->im);
        return [
            'hash' => $code['hash'],
            'content' => base64_encode($content)
        ];
    }

    /**
     * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
     *
     *      高中的数学公式咋都忘了涅，写出来
     *        正弦型函数解析式：y=Asin(ωx+φ)+b
     *      各常数值对函数图像的影响：
     *        A：决定峰值（即纵向拉伸压缩的倍数）
     *        b：表示波形在Y轴的位置关系或纵向移动距离（上加下减）
     *        φ：决定波形与X轴位置关系或横向移动距离（左加右减）
     *        ω：决定周期（最小正周期T=2π/∣ω∣）
     *
     */
    private function writeCurve(): void {
        $px = $py = 0;

        // 曲线前部分
        $A = mt_rand(1, $this->imageH / 2); // 振幅
        $b = mt_rand(-$this->imageH / 4, $this->imageH / 4); // Y轴方向偏移量
        $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量
        $T = mt_rand($this->imageH, $this->imageW * 2); // 周期
        $w = (2 * M_PI) / $T;

        $px1 = 0; // 曲线横坐标起始位置
        $px2 = mt_rand($this->imageW / 2, $this->imageW * 0.8); // 曲线横坐标结束位置

        for ($px = $px1; $px <= $px2; $px = $px + 1) {
            if (0 != $w) {
                $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
                $i = (int)($this->fontSize / 5);
                while ($i > 0) {
                    imagesetpixel($this->im, $px + $i, $py + $i, $this->color); // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出（不用这while循环）性能要好很多
                    $i--;
                }
            }
        }

        // 曲线后部分
        $A = mt_rand(1, $this->imageH / 2); // 振幅
        $f = mt_rand(-$this->imageH / 4, $this->imageH / 4); // X轴方向偏移量
        $T = mt_rand($this->imageH, $this->imageW * 2); // 周期
        $w = (2 * M_PI) / $T;
        $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2;
        $px1 = $px2;
        $px2 = $this->imageW;

        for ($px = $px1; $px <= $px2; $px = $px + 1) {
            if (0 != $w) {
                $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
                $i = (int)($this->fontSize / 5);
                while ($i > 0) {
                    imagesetpixel($this->im, $px + $i, $py + $i, $this->color);
                    $i--;
                }
            }
        }
    }

    /**
     * 画杂点（往图片上写不同颜色的字母或数字）
     */
    private function writeNoise(): void {
        $codeSet = '2345678abcdefhijkmnpqrstuvwxyz';
        for ($i = 0; $i < 10; $i++) {
            //杂点颜色
            $noiseColor = imagecolorallocate($this->im, mt_rand(150, 225), mt_rand(150, 225), mt_rand(150, 225));
            for ($j = 0; $j < 5; $j++) {
                // 绘杂点
                imagestring($this->im, 5, mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $codeSet[mt_rand(0, 29)], $noiseColor);
            }
        }
    }

    /**
     * 生成新验证码
     * @return array
     */
    private function generateCode(): array {
        $characters = str_split($this->codeSet);

        $bag = '';
        for ($i = 0; $i < $this->length; $i++) {
            $bag .= $characters[rand(0, count($characters) - 1)];
        }

        $hash = md5(msectime() . rand_char(10) . $bag);
        // 5分钟内有效
        $this->getRedis()->setex($this->getRedisKey($hash), 300, $bag);

        return [
            'value' => $bag,
            'hash' => $hash,
        ];
    }

    /**
     * redis key
     * @param string $hash
     * @return string
     */
    private function getRedisKey(string $hash): string {
        return 'yunj.library.captcha:' . $hash;
    }

    /**
     * 验证（有可能有并发问题，后面可采用lua处理）
     */
    public function check(string $code, string $hash): bool {
        if (!is_md5_result($hash)) return false;
        $redis = $this->getRedis();
        $key = $this->getRedisKey($hash);
        $_code = $redis->get($key);
        $res = strtolower($_code) === strtolower($code);
        if ($res) {
            $redis->del($key);
        }
        return $res;
    }

}